AutoScallingのライフサイクルフックを使用してスケールイン時にEC2インスタンス内のログを退避させる
はじめに
こんにちは。大阪オフィスの林です。
EC2 AutoScalling環境下でEC2インスタンスがターミネートする際のログ退避についてライフサイクルフックを使用したアーキテクチャーを検討したのでまとめておきたいと思います。 今回検討したアーキテクチャーはザックリ下図のイメージです。
よくあるログ管理(退避)のアーキテクチャとして、CloudWatchAgentを使用しCloudWatchLogsにログを転送し、Kinesisを挟んでS3に転送するといった方法もありますが、CloudWatchLogs自体の料金がネックとなり採用を見送るケースもあったりします。かと言ってOS上で定期的な間隔でログ転送バッチを回したりするのも良いのですが、その定期的な間隔とEC2インスタンスのターミネートのタイミング次第では直近のログが欠陥してしまうことも十分考えられます。今回のアーキテクチャはその折衷案というような位置付けで捉えて頂ければ幸いです。
やってみた
ログ格納用S3バケットの準備
まずは下記構成図の赤点線部分のS3バケットを作っていきます。
ログ格納用のS3バケットはデフォルトの設定で問題ありません。次の工程で「バケット名」を使うのでバケット名は控えておいてください。
SSM Documentの準備
次に、下記構成図の赤点線部分のSSM Documentを作成していきます。
Systems Managerのダッシュボードから「共有リソース」-「ドキュメント」を選択し「Create document」-「Command or Session」を選択します。
任意の名前を入力し、ターゲットオプションに「/AWS::EC2::Instance」を選択、ドキュメントタイプに「コマンドドキュメント」を選択します。
コンテンツエリアで「YAML」を選択し下記のコマンドをコピペします。対象のログは実際の環境で対象としたいログファイル/ログファイルのパスに置き換えてください。また、「バケット名」には前工程で作成したログ格納用S3バケットの名前を指定します。
{ "schemaVersion": "2.2", "description": "log upload", "parameters": {}, "mainSteps": [ { "action": "aws:runShellScript", "name": "configureServer", "inputs": { "runCommand": [ "instanceid=(`ec2-metadata -i | cut -d ' ' -f 2`)", "aws s3 cp /var/log/php-fpm/error.log s3://バケット名/$instanceid/" # 複数ログを対象とする場合は、下記のようにカンマ(,)で繋げでください。最終行のみカンマ(,)は不要です。 # "aws s3 cp /var/log/php-fpm/error.log s3://バケット名/$instanceid/", # "aws s3 cp /var/log/php-fpm/xxxxx.log s3://バケット名/$instanceid/", # "aws s3 cp /var/log/php-fpm/yyyyy.log s3://バケット名/$instanceid/" ] } } ] }
Lambdaの準備
次に、下記構成図の赤点線部分のLambda関数を作成していきます。
コードはPython3.8で作成しています。コード自体はシンプルでEventBridge経由で受け取ったイベントのデータからインスタンスIDを抽出して、Run Command実行時の対象EC2として指定をしています。
import json import boto3 ssm = boto3.client('ssm') def lambda_handler(event, context): EC2InstanceId = event['detail']['EC2InstanceId'] response = ssm.send_command( InstanceIds = [EC2InstanceId], DocumentName = "log-upload" #前工程で作成したSSM Documentの名前を指定する。 ) return
Lambda関数からSSM Documentを叩くため、Lambda関数にアタッチされているロールに必要な権限を付与しておきます。「設定」-「アクセス権限」からアタッチされているロールを選択します。
今回はAmazonSSMFullAccess
を割り当てておきます。
EventBridgeの準備
次に、下記構成図の赤点線部分赤点線部分のEventBridgeのルールを作成していきます。
EventBridgeのダッシュボードから「ルールを作成」を選択します。
パターン定義で「イベントパターン」-「カスタムパターン」を選択し、イベントパターンに下記のパターンをコピペし「保存」します。
{ "source": ["aws.autoscaling"], "detail-type": ["EC2 Instance-terminate Lifecycle Action"] }
ターゲットに「Lambda関数」を選択し、前の工程で作成したLambda関数を指定します。
EventBridgeのルールが作成されたことを確認します。
ライフサイクルフックの設定
最後に、下記構成図の赤点線部分のライフサイクルフックの設定を実施していきます。
EC2のダッシュボードから「Auto Scalling Groups」を選択し、対象となるAuto Scallingグループ名を選択します。
「インスタンス管理」タブを選択し「ライフサイクルフックを作成」を選択します。
任意の名前を入力し、ライフサイクル移行に「インスタンス終了」を選択、ハートビートタイムアウトに任意の秒数、デフォルトの結果に「CONTINUE」を選択し作成します。
上記部分のパラメータについて補足をしておきます。
- ライフサイクル移行
- ハートビートタイムアウト
- デフォルトの結果
ライフサイクルフックには「起動ライフサイクルフック」と「終了ライフサイクルフック」の2種類があり、今回は「終了ライフサイクルフック(インスタンス終了)」を使用してインスタンスのターミネートを遅らせ、その間にログの退避をする構成としています。もう一方の「起動ライフサイクルフック」を使用するケースとしては、AutoScallingの起動設定やベースAMIに組み込めない設定や作業をスケールアウトのタイミングに行うケースなどが考えられます。
EC2インスタンスの起動またはターミネートを遅らせる時間を「ハートビートタイムアウト」の時間として設定します。この「ハートビートタイムアウト」の秒数は設計が必要になる部分なのですが、必要な作業が完了する十分な時間を割り当てておくと良いでしょう。「ハートビートタイムアウト」は下図の青色部分を指し、起動またはターミネートの「Wait」として設定されることとなります。
引用:https://docs.aws.amazon.com/ja_jp/autoscaling/ec2/userguide/lifecycle-hooks.html
ライフサイクルフックの使い方には、「起動ライフサイクルフック」または「終了ライフサイクルフック」での処理が終わった際、ライフサイクルフックに対して「完了した!」とユーザーからアクションして、対象インスタンスの状態を上図フローの「Proceed」に進めてもらう方法があります。今回はそのような同期的な処理までは考慮していないので、ライフサイクルフックに対して「完了した!」とアクションを何もせずに次の「Proceed」に進んでもらうため「CONTINUE」としています。厳密に同期的な処理を取り入れたい場合などは「CONTINUE」ではなく「ABANDON」の設定を検討頂ければと思います。
少し前の記事ですがライフサイクルフックについての詳細は下記を参照いただくと、より理解も深めて頂けるかと思います。
権限周りなどの注意点
このアーキテクチャーでは、EC2からS3に対してコマンドを実行しているので、EC2からS3に対してのPutObject権限が必要になります。
また、Run Commandを使ってEC2に対してコマンドを実行するために、EC2インスタンスがSSMのマネージドインスタンスになっていることが前提となります。
まとめ
EC2 AutoScalling環境下でEC2インスタンスがターミネートする際のログ退避を検討したいけど、CloudWatchLogsはコストが、、、とお悩みの方の参考になれば幸いです!
以上、大阪オフィスの林がお送りしました!